/*
 * Copyright 2022 The Open Islands Authors. All Rights Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package org.openislands.oi.util;

import org.springframework.beans.BeanUtils;
import sun.reflect.generics.reflectiveObjects.ParameterizedTypeImpl;

import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class ReflectUtils {

    /**
     * Create an instance through the default null-argument constructor.
     * If the creation fails, get the first constructor to create
     *
     * @param clazz instance class
     * @param <T>   type
     * @return instance
     */
    @SuppressWarnings("unchecked")
    public static <T> T instantiateClass(Class<T> clazz) {
        try {
            return BeanUtils.instantiateClass(clazz);
        } catch (Exception ignored) {
            Constructor<?>[] declaredConstructors = clazz.getDeclaredConstructors();
            Constructor<?> constructor = declaredConstructors[0];
            return (T) BeanUtils.instantiateClass(constructor, new Object[constructor.getParameterCount()]);
        }
    }

    /**
     * First find the method declaring the class by the method name and parameter list.
     * If not found, get the first method with the same name in declared class
     *
     * @param clazz      declared class
     * @param methodName match method name
     * @param params     some method parameters
     * @return method
     * @throws NoSuchMethodException No method with matching method name found in class
     */
    public static Method findAnyClassMethod(Class<?> clazz, String methodName, Object... params) throws NoSuchMethodException {
        try {
            final Class<?>[] parameterTypes = Arrays.stream(params).map(Object::getClass).toArray(Class[]::new);
            return clazz.getMethod(methodName, parameterTypes);
        } catch (Exception e) {
            Method[] methods = clazz.getMethods();
            List<Method> methodList = Arrays.stream(methods).filter(m -> methodName.equals(m.getName())).collect(Collectors.toList());
            if (methodList.isEmpty()) throw new NoSuchMethodException(clazz + "." + methodName);
            return methodList.get(0);
        }
    }

    /**
     * Find the {@param interfaceClass} of the instance class {@param clazz}.
     * Get the generic {@param index} location class
     *
     * @param clazz          instance class
     * @param interfaceClass implements interface
     * @param index          generic index location
     * @param <T>            generic type
     * @return generic class
     */
    @SuppressWarnings("unchecked")
    public static <T> Class<T> findInterfacesGenericClass(Class<?> clazz, Class<?> interfaceClass, int index) {
        Type[] genericInterfaces = clazz.getGenericInterfaces();
        for (Type genericInterface : genericInterfaces) {
            ParameterizedType parameterizedType;
            if (genericInterface instanceof ParameterizedType
                    && (parameterizedType = (ParameterizedTypeImpl) genericInterface).getRawType() == interfaceClass) {
                return (Class<T>) parameterizedType.getActualTypeArguments()[index];
            } else if (genericInterface instanceof Class && interfaceClass.isAssignableFrom(((Class<?>) genericInterface))) {
                return findInterfacesGenericClass((Class<?>) genericInterface, interfaceClass, index);
            }
        }
        throw new RuntimeException("The " + interfaceClass + " not assignable from " + clazz);
    }
}
